import {Tokenizer} from './Tokenizer.js';

export default class Parser{
  constructor(){
    this._string = '';
    this._tokenizer = new Tokenizer();
  }

  parse(string){
    this._string = string;
    this._tokenizer.init(string);
    this._currentToken = this._tokenizer.getNextToken();
    return this.Program();
  }
  Statement(){
    switch(this._currentToken.type){
      case ';':
        return this.EmptyStatement();
      case 'if':
        return this.IfStatement();
      case '{':
        return this.ClassDeclaration();
      case 'let':
          return this.VariableStatement();
      case 'function':
        return this.FunctionDeclaration();
      case 'class':
        return this.ClassDeclaration();
      case 'return':
        return this.ReturnStatement();
      case 'while':
      case 'do':
      case 'for':
        return this.IterationStatement();
      default:
        return this.ExpressionStatement();
    }
  }
  _eat(tokentype){
    const token = this._currentToken;
    if(token === null){
      throw new SyntaxError(
        `xxUnexpected end of input, expected: "${tokenType}"`
      )
    }
    
    if(token.type !== tokentype){
      throw new SyntaxError(
        `xxUnexpected tokentype: "${tokentype}"`
      )
    }
    this._currentToken = this._tokenizer.getNextToken();
    return token;
  }
  Identifier(){
    const name = this._eat('IDENTIFIER').value;
    return {type:'Identifier',name}
  }
  Program(){
    return {
      type : 'Proigram',
      body:this.StatementList()
    } 
  }

  StatementList(stopLookahead = null){
    const statementList = [this.Statement()];
    
    while(
      this._currentToken !== null &&
      this._currentToken.type !== stopLookahead
    ){
      statementList.push(this.Statement())
    }
    return statementList;
  }

  ReturnStatement(){
    this._eat('return');
    const argument = 
    this._currentToken.type !=';'? this.Expression():null;
    this._eat(';');
    return {
      type : 'ReturnStatement',argument
    }
  }
  ClassDeclaration(){
    this._eat('class');
    const id = this.Identifier();
    const superClass = 
    this._currentToken.type === 'extends' ? this.ClassExtends():null;
    const body = this.BlockStatement();

    return {
      type :'ClassDeclaration',
      id,
      superClass,
      body
    }
  }
  ClassExtends(){
    this._eat('extends');
    return this.Identifier();
  }
  ReturnStartment(){
    this._eat('return');
    const argument = 
    this._currentToken.type !==';' ? this.Expression() : null;
    this._eat(';');
    
    return {type:'ReturnStatement',argument}
  }

  FunctionDeclaration(){
    
    this._eat('function');
    const name = this.Identifier();
    this._eat('(');
    const params = 
    this._currentToken.type !==')' ? this.FormalParameterList():[];
    this._eat(')');
    const body = this.BlockStatement();

    return {
      type:'FunctionDeclaration',
      name,params,body
    }
  }

  FormalParameterList(){
    const result = [];
    do{
      result.push(this.Identifier());
    }while(this._currentToken.type === ',' && this._eat(','));
    return result;
  }
  IterationStatement(){
    switch(this._currentToken.type){
      case 'while':
        return this.WhileStatement();
      case 'do':
        return this.DoWhileStatement();
      case 'for':
        return this.ForStatement();
    }
  }

  WhileStatement(){
    this._eat('while');
    this._eat('(');
    const test = this.Expression();
    this._eat(')');
    const body = this.Statement();
    return {
      type:'WhileStatement',test,body
    }
  }
  DoWhileStatement() {
    this._eat('do');
    const body = this.BlockStatement();
    this._eat('while');
    this._eat('(');
    const test = this.Expression();
    this._eat(')');
    this._eat(';');

    return {
        type: 'DoWhileStatement',
        body,
        test,
    };
  }

  ForStatement(){
    this._eat('for');
    this._eat('(');

    const init = 
    this._currentToken.type !== ';' ? this.ForStatementInit() :null;

    const test = this._currentToken.type !== ';' ? this.Expression() : null;
    this._eat(';');

    const update =
        this._currentToken.type !== ')' ? this.Expression() : null;
    this._eat(')');

    const body = this.Statement();

    return {
      type: 'ForStatement',
      init,
      test,
      update,
      body,
    };
  }

  ForStatementInit(){
    if(this._currentToken.type === 'let'){
      return this.VariableStatementInit();
    }
    return this.Expression()
  }

  IfStatement(){
    this._eat('if');
    this._eat('(');
    const test = this.Expression();
    this._eat(')');
    const consequent = this.Statement();
    const alternate = 
    this._currentToken !== null && this._currentToken.type ==='else'
    ? this._eat('else') && this.Statement(): null;
    
    return {
      type:'IfStatement',
      test,consequent,alternate
    }
  }

  VariableStatementInit(){
    this._eat('let');
    const declarations = this.VariableDeclarationList();
    return {type:'VariableStatement',declarations}
  }
  VariableStatement() {
    const variableStatement = this.VariableStatementInit();
    this._eat(';');
    return variableStatement;
  }

  VariableDeclarationList() {
    const declarations = [];
    do {
        declarations.push(this.VariableDeclaration());
    } while (this._currentToken.type === ',' && this._eat(','));

    return declarations;
  }

  VariableDeclaration(){
    const id = this.Identifier();

    const init = 
    this._currentToken.type !== ',' && this._currentToken.type !== ';'
    ? this.VariableInitializer() : null;
    return {
      type:'VariableDeclaration',id,init
    }
  }

  VariableInitializer() {
    this._eat('SIMPLE_ASSIGN');
    return this.AssignmentExpression();
  }

  EmptyStatement() {
    this._eat(';');
    return {
        type: 'EmptyStatement',
    };
  }

  BlockStatement(){
    this._eat('{');
    const body = 
    this._currentToken.type !== '}'?this.StatementList('}'):[];
    this._eat('}')
    return {
      type:'BlockStatement',body
    }
  }
  ExpressionStatement() {
    const expression = this.Expression();
    // Every expression must end with ';'
    this._eat(';');
    return {
        type: 'ExpressionStatement',
        expression,
    };
  }

  Expression() {
    return this.AssignmentExpression();
  }
  AssignmentExpression() {
    const left = this.LogicalORExpression();

    if (!this._isAssignmentOperator(this._currentToken.type)) {
        return left;
    }

    return {
        type: 'AssignmentExpression',
        operator: this.AssignmentOperator().value,
        left: this._checkValidAssignmentTarget(left),
        right: this.AssignmentExpression(),
    };
  }
  Literal(){
    switch (this._currentToken.type) {
      case 'NUMBER':
        return this.NumericLiteral();
      case 'STRING':
        return this.StringLiteral();
      case 'true':
        return this.BooleanLiteral(true);
      case 'false':
        return this.BooleanLiteral(false);
      case 'null':
        return this.NullLiteral();
      default:
        throw new SyntaxError('Literal error');
    }
  }

  NumericLiteral() {
    const token = this._eat('NUMBER');
    return {
      type: 'NumericLiteral',
      value: token.value,
    };
  }
  StringLiteral() {
    const token = this._eat('STRING');
    return {
      type: 'StringLiteral',
      value: token.value.slice(1, -1),
    };
  }
  BooleanLiteral(value) {
    this._eat(value ? 'true' : 'false');
    return {
      type: 'BooleanLiteral',
      value,
    };
  }

  NullLiteral() {
    this._eat('null');
    return {
      type: 'NullLiteral',
      value: null,
    };
  }

  _checkValidAssignmentTarget(node) {
    if (node.type === 'Identifier' || node.type === 'MemberExpression') {
      return node;
    }
    throw new SyntaxError(
      '_checkValidAssignmentTarget'
    );
  }
  _isAssignmentOperator(tokenType) {
    return tokenType === 'SIMPLE_ASSIGN' || tokenType === 'COMPLEX_ASSIGN';
  }
  AssignmentOperator() {
    if (this._currentToken.type === 'SIMPLE_ASSIGN') {
      return this._eat('SIMPLE_ASSIGN');
    }
    return this._eat('COMPLEX_ASSIGN');
  }

  LogicalORExpression() {
    return this._LogicalExpression('LogicalANDExpression', 'LOGICAL_OR');
  }
  LogicalANDExpression() {
    return this._LogicalExpression('EqualityExpression', 'LOGICAL_AND');
  }
  _LogicalExpression(builderName, operatorToken) {
    let left = this[builderName]();

    while (this._currentToken.type === operatorToken) {
      const operator = this._eat(operatorToken).value;
      const right = this[builderName]();
      left = {
          type: '_LogicalExpression',
          operator,
          left,
          right,
      };
    }

    return left;
  }

  EqualityExpression() {
    return this._BinaryExpression(
      'RelationalExpression',
      'EQUALITY_OPERATOR'
    );
  }

  _BinaryExpression(builderName, operatorToken) {
    let left = this[builderName]();

    while (this._currentToken.type === operatorToken) {
      const operator = this._eat(operatorToken).value;
      const right = this[builderName]();
      left = {
          type: 'BinaryExpression',
          operator,
          left,
          right,
      };
    }

    return left;
  }

  RelationalExpression() {
    return this._BinaryExpression(
        'AdditiveExpression',
        'RELATIONAL_OPERATOR'
    );
  }

  AdditiveExpression() {
    return this._BinaryExpression(
      'MultiplicativeExpression',
      'ADDITIVE_OPERATOR'
    );
  }

  MultiplicativeExpression() {
    return this._BinaryExpression(
      'UnaryExpression',
      'MULTIPLICATIVE_OPERATOR'
    );
  }

  UnaryExpression() {
    let operator;
    switch (this._currentToken.type) {
      case 'ADDITIVE_OPERATOR':
        operator = this._eat('ADDITIVE_OPERATOR').value;
        break;
      case 'LOGICAL_NOT':
        operator = this._eat('LOGICAL_NOT').value;
        break;
    }
    if (operator) {
      return {
        type: 'UnaryExpression',
        operator,
        argument: this.UnaryExpression(),
      };
    }
    return this.LeftHandSideExpression();
  }

  LeftHandSideExpression() {
    return this.CallMemberExpression();
  }
  CallMemberExpression() {
    if (this._currentToken.type === 'super') {
      return this._CallExpression(this.Super());
    }
    const member = this.MemberExpression();
    if (this._currentToken.type === '(') {
      return this._CallExpression(member);
    }

    return member;
  }

  _CallExpression(callee) {
    let callExpression = {
      type: 'CallExpression',
      callee,
      arguments: this.Arguments(),
    };

    if (this._currentToken.type === '(') {
      callExpression = this._CallExpression(callExpression);
    }

    return callExpression;
  }

  Arguments(){
    this._eat('(');
    const argumentList =
    this._currentToken.type !== ')' ? this.ArgumentList() :[];
    this._eat(')');
    return argumentList;
  }

  ArgumentList(){
    const argumentList = [];
    do{
      argumentList.push(this.AssignmentExpression());
    }while(this._currentToken.type === ',' && this._eat(','))
    return argumentList;
  }

  MemberExpression() {
    let object = this.PrimaryExpression();

    while (
      this._currentToken.type === '.' ||
      this._currentToken.type === '['
    ) {
      // MemberExpression '.' Identifier
      if (this._currentToken.type === '.') {
        this._eat('.');
        const property = this.Identifier();
        object = {
            type: 'MemberExpression',
            computed: false,
            object,
            property,
        };
      }

      // MemberExpression '[' Expression ']'
      if (this._currentToken.type === '[') {
        this._eat('[');
        const property = this.Expression();
        this._eat(']');
        object = {
          type: 'MemberExpression',
          computed: true,
          object,
          property,
        };
      }
    }

    return object;
  }

  PrimaryExpression() {
    if (this._isLiteral(this._currentToken.type)) {
      return this.Literal();
    }
    switch (this._currentToken.type) {
      case '(':
        return this.ParenthesizedExpression();
      case 'IDENTIFIER':
        return this.Identifier();
      case 'this':
        return this.ThisExpression();
      case 'new':
        return this.NewExpression();
      default:
        throw new SyntaxError('Unexpected primary expression.');
    }
  }

  NewExpression() {
    this._eat('new');
    return {
      type: 'NewExpression',
      callee: this.MemberExpression(),
      arguments: this.Arguments(),
    };
  }

  ThisExpression() {
    this._eat('this');
    return {
      type: 'ThisExpression',
    };
  }

  Super() {
    this._eat('super');
    return {
      type: 'Super',
    };
  }

  _isLiteral(tokenType) {
    return (
      tokenType === 'NUMBER' ||
      tokenType === 'STRING' ||
      tokenType === 'true' ||
      tokenType === 'false' ||
      tokenType === 'null'
    );
  }

  ParenthesizedExpression() {
    this._eat('(');
    const expression = this.Expression();
    this._eat(')');
    return expression;
  }
}
const str = `
let bianl = '155';
let num = 15;
function asd(par){
  return par;
};
`
const code = new Parser().parse(str) 
console.log(code);
